Create New Feature Screen

Before you dive in, it may be helpful to get familiar with the companion document Screen Structure and Wiring, which explains how the individual classes fit together. With that context in mind, this guide walks you through creating a new feature screen in the project. You can choose one of three paths:

  1. Using file templates — the recommended, fast and stable way
  2. Ask an AI agent — automated, the fastest way
  3. Manual setup — when you need full control or when templates aren't available

Option 1: Using File Templates (recommended)

  1. Ensure the UI Feature template is installed
    • File → Manage IDE Settings → Import Settings...
    • Import file_teamplates.zip from the baselines-kmp root dir
  2. Right-click the destination package → New → UI Feature
  3. Enter the feature name (e.g., Profile, Settings)
  4. Review generated files and fix missing imports
  5. Done 🎉 — your feature is wired into the app

Option 2: Setup via AI Agent

  1. Open chat with your AI assistant and make sure it's aware of ./AGENTS.md file.
  2. Ask to create new feature screen
  3. Provide all the necessary info requested by the AI agent
  4. Done 🎉 — your feature is wired into the app

Option 3: Manual Setup

  1. Create *UiEvent
kotlin
1sealed interface ProfileUiEvent : UiEvent {
2 data object PerformLogout : ProfileUiEvent {
3 override val dispatchPolicy = UiEventDispatchPolicy.ThrottleFirst()
4 }
5}

Use ThrottleFirst for click-like one-shot events. Use DebounceLatest only for delayed latest-value effects such as search or filtering. If a text field is controlled by ViewModel state, keep the visible input state immediate and debounce a separate effect event instead of the input-state event itself.

  1. Create *UiState
kotlin
1@Immutable
2data class ProfileUiState(
3 override val eventSink: (ProfileUiEvent) -> Unit,
4) : UiState<ProfileUiEvent>
  1. Create *ViewModel

Use plain @Inject for the normal case. Switch to @AssistedInject only when the ViewModel needs runtime arguments that cannot come from the DI graph.

kotlin
1@Inject
2@ContributesIntoMap(AppScope::class, binding<ViewModel>())
3@ViewModelKey(ProfileViewModel::class)
4class ProfileViewModel : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {
5
6 private val eventSink = createEventSink(::handleEvent)
7
8 @Composable
9 override fun state() = ProfileUiState(
10 eventSink = eventSink,
11 )
12
13 private fun handleEvent(event: ProfileUiEvent) {
14 when (event) {
15 ProfileUiEvent.PerformLogout -> handleLogout()
16 }
17 }
18
19 private fun handleLogout() {
20 /* handle action */
21 }
22}
  1. Create *Screen
kotlin
1@Composable
2fun ProfileScreen(onLogoutClicked: () -> Unit) {
3 /* UI */
4}
  1. Create *Route
kotlin
1@Composable
2fun ProfileRoute(viewModel: ProfileViewModel) {
3 val state = viewModel.state()
4 val eventSink = state.eventSink
5 ProfileScreen(
6 onLogoutClicked = { eventSink(ProfileUiEvent.PerformLogout) }
7 )
8}
  1. Add DI *Module
kotlin
1@ContributesTo(UiScope::class)
2interface ProfileUiModule {
3
4 @Provides
5 @IntoSet
6 fun provideProfileNavGraphEntry(): NavGraphEntry = NavGraphEntry {
7 composable<AppNavRoutes.Profile> {
8 ProfileRoute(metroViewModel())
9 }
10 }
11}
  1. Ensure the feature module is a dependency of app/compose module
  2. Done 🎉 — your feature is wired into the app